﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Windows.Documents;
using System.Collections.Specialized;
using System.Windows;
using ShredTheLog.Model;
using System.Windows.Data;
using System.Globalization;
using System.Diagnostics;
using ShredTheLog.Api.DataContracts.TextChat;
using ShredTheLog.Api.DataContracts;
using System.ComponentModel.Composition;
using ShredTheLog.Api;
using System.Windows.Threading;
using ShredTheLog.Translation;
using ShredTheLog.Controls;
using ShredTheLog.Common.MVVM;
using System.Disposables;
using System.ComponentModel;

namespace ShredTheLog.ViewModels
{
    public sealed class TextChatRoomViewModel : ViewModelBase, IDisposable
    {
        [Import]
        private IShredServices ShredServices { get; set; }

        [Import]
        private IUiSettingsSection UiSettings { get; set; }

        [Import]
        private IConnectionSettingsSection ConnectionSettings { get; set; }

        [Import]
        private IMessageNotificationSource MessageNotificationSource { get; set; }

        [Import]
        public IShowLogInDialogCommand ShowLogInDialogCommand { get; private set; }

        private TextChatRoomUserInfo Me { get; set; }
        private IDisposable _whenRoomOpenedSubscription;
        private CompositeDisposable _subscriptions = new CompositeDisposable();

        public TextChatRoomViewModel(string roomName)
        {
            Contract.Requires(!String.IsNullOrWhiteSpace(roomName));

            RoomName = roomName;
            Users = new ReadOnlyObservableCollection<TextChatRoomUserInfo>(_internalUsers);
            Messages = new ReadOnlyObservableCollection<IMessage>(_internalMessages);

            var whenRoomOpened =
                from i in ShredServices.TextChat.WhenRoomOpened
                where i.RoomName == roomName
                select i;
            Action<Exception> onRoomOpenError =
                ex =>
                {
                    _whenRoomOpenedSubscription.Dispose();
                    _whenRoomOpenedSubscription = null;
                    IsLoading = false;
                    IsWindowClosed = true;
                    ShredServices.ShowError(ex);
                };
            _whenRoomOpenedSubscription = whenRoomOpened.ObserveOn(ShredServices.Dispatcher).Subscribe(
                initialInfo =>
                {
                    try
                    {
                        if (!IsWindowClosed)
                        {
                            _whenRoomOpenedSubscription.Dispose();
                            _whenRoomOpenedSubscription = null;
                            OnRoomOpened(initialInfo);
                            IsLoading = false;
                        }
                    }
                    catch (Exception ex)
                    {
                        onRoomOpenError(ex);
                    }
                },
                onRoomOpenError);

            
            var textChat = ShredServices.TextChat;
            Contract.Assume(textChat != null);
            textChat.OpenRoom(RoomName);
        }

        private void OnRoomOpened(IEnterRoomUpdate initialInfo)
        {
            Contract.Requires(initialInfo != null);

            if (Credentials != null)
            {
                var proto = ShredServices.TextChatUsers.Single(u => u.Uid == Credentials.Uid);
                Contract.Assume(proto != null);
                Me = new TextChatRoomUserInfo(proto);
            }

            if (Me != null)
                _internalUsers.Add(Me);

            var initialUsers =
                from iu in initialInfo.Users
                join tu in ShredServices.TextChatUsers on iu.Uid equals tu.Uid
                select new TextChatRoomUserInfo(tu);

            _internalUsers.AddRange(initialUsers);

            var whenUserEntersRoomSubscription = ShredServices.TextChat.WhenUserEntersRoom.ObserveOn(ShredServices.Dispatcher).Subscribe(
                userToAdd =>
                {
                    try
                    {
                        if (userToAdd.RoomName == RoomName)
                        {
                            if (_internalUsers.Any(i => i.Uid == userToAdd.Uid))
                                throw new InvalidOperationException(String.Format("User {0:d} has been already added.", userToAdd.Uid));
                            var u = ShredServices.TextChatUsers.Single(i => i.Uid == userToAdd.Uid);
                            _internalUsers.Add(new TextChatRoomUserInfo(u));
                        }
                    }
                    catch (Exception ex)
                    {
                        ShredServices.ShowError(ex);
                    }
                },
                ShredServices.ShowError);
            _subscriptions.Add(whenUserEntersRoomSubscription);

            var whenUserLeavesRoomSubscription = ShredServices.TextChat.WhenUserLeavesRoom.ObserveOn(ShredServices.Dispatcher).Subscribe(
               userToDelete =>
               {
                   try
                   {
                       if (userToDelete.RoomName == RoomName)
                       {
                           var u = _internalUsers.Single(i => i.Uid == userToDelete.Uid);
                           _internalUsers.Remove(u);
                       }
                   }
                   catch (Exception ex)
                   {
                       ShredServices.ShowError(ex);
                   }
               },
               ShredServices.ShowError);
            _subscriptions.Add(whenUserLeavesRoomSubscription);

            var whenUserTypesSubscription = ShredServices.TextChat.WhenUserTypes.ObserveOn(ShredServices.Dispatcher).Subscribe(
               user =>
               {
                   try
                   {
                       if (user.RoomName == RoomName)
                           _internalUsers.Single(i => i.Uid == user.Uid).LastActivity = DateTime.Now;
                   }
                   catch (Exception ex)
                   {
                       ShredServices.ShowError(ex);
                   }
               },
               ShredServices.ShowError);
            _subscriptions.Add(whenUserTypesSubscription);

            _internalMessages.AddRange(initialInfo.Messages);
            var whenMessageReceivedSubscription = ShredServices.TextChat.WhenMessageReceived.ObserveOn(ShredServices.Dispatcher).Subscribe(
                message =>
                {
                    try
                    {
                        if (message.RoomName == RoomName)
                        {
                            var textChat = ShredServices.TextChat;
                            bool isMyMessage = textChat != null && textChat.Credentials != null && textChat.Credentials.Uid == message.Uid;
                            if (!isMyMessage)
                                MessageNotificationSource.OnNext(message);

                            _internalMessages.Add(new MessageEx(ShredServices.TextChatUsers.Single(i => i.Uid == message.Uid), isMyMessage, message.Text));
                            Trace.WriteLineIf(TraceFlags.MessagesConverter, "Updating messages...");
                            OnPropertyChanged(() => Messages);
                        }
                    }
                    catch (Exception ex)
                    {
                        ShredServices.ShowError(ex);
                    }
                },
                ShredServices.ShowError);
            _subscriptions.Add(whenMessageReceivedSubscription);
            OnPropertyChanged(() => Messages);

            // Отсылка нотификаций о наборе текста
            _typingNotifyTimer = new DispatcherTimer(
                TimeSpan.FromSeconds(ConnectionSettings.GetClient().UpdatePeriod.TotalSeconds / 2.0),
                DispatcherPriority.Normal,
                (sender, e) =>
                {
                    if (Me != null)
                    {
                        Me.OnTypingChanged();
                        if (Me.IsTyping && UiSettings.SendTypingNotifications)
                            ShredServices.TextChat.Update(new FlashPenCommand() { RoomName = this.RoomName });
                    }

                    _internalUsers.ForEach(u => u.OnTypingChanged());
                },
                ShredServices.Dispatcher);
        }

        private DispatcherTimer _typingNotifyTimer;

        public ISessionCredentials Credentials { get { return ShowLogInDialogCommand.Credentials; } }

        public string RoomName { get; private set; }

        public bool IsLoading { get { return _isLoading; } private set { _isLoading = value; OnPropertyChanged(() => IsLoading); } }
        private bool _isLoading = true;

        private ObservableCollection<TextChatRoomUserInfo> _internalUsers = new ObservableCollection<TextChatRoomUserInfo>();
        public ReadOnlyObservableCollection<TextChatRoomUserInfo> Users { get; private set; }

        private string _newMessage = String.Empty;
        public string NewMessage 
        { 
            get { return _newMessage; } 
            set 
            {
                _newMessage = value;
                OnPropertyChanged(() => NewMessage);

                if (UiSettings.SendTypingNotifications)
                    Me.LastActivity = DateTime.Now;
            } 
        }

        private ObservableCollection<IMessage> _internalMessages = new ObservableCollection<IMessage>();
        public ReadOnlyObservableCollection<IMessage> Messages { get; private set; }
        public KeyGesture SendMessageGesture { get { return UiSettings.SendMessageGesture; } }

        private ICommand _sendMessageCommand;
        public ICommand SendMessageCommand
        {
            get
            {
                if (_sendMessageCommand == null)
                {
                    Action sendMessageAction =
                        () => 
                        {
                            Contract.Assume(Credentials != null, "User is not logged in. Message has not been sent.");
                            if (!String.IsNullOrEmpty(NewMessage))
                            {
                                string message = NewMessage.TrimEnd(' ', '\r', '\n');
                                if (!String.IsNullOrEmpty(message))
                                {
                                    ShredServices.TextChat.SendMessage(RoomName, message);
                                    NewMessage = String.Empty;
                                }
                            }
                        };
                    _sendMessageCommand = new DelegateCommand(sendMessageAction, () => Credentials != null && !String.IsNullOrEmpty(NewMessage) && !String.IsNullOrEmpty(NewMessage.TrimEnd(' ', '\r', '\n')));
                }
                return _sendMessageCommand;
            }
        }

        private bool _isWindowClosed;
        public bool IsWindowClosed
        {
            get { return _isWindowClosed; }
            set
            {
                if (_isWindowClosed != value)
                {
                    _isWindowClosed = value;
                    OnPropertyChanged(() => IsWindowClosed);
                    if (IsWindowClosed)
                        Dispose();
                }
            }
        }

        public void Dispose()
        {
            if (_whenRoomOpenedSubscription != null)
            {
                _whenRoomOpenedSubscription.Dispose();
                _whenRoomOpenedSubscription = null;
            }
            _subscriptions.Dispose();
            if (_typingNotifyTimer != null)
            {
                _typingNotifyTimer.Stop();
                _typingNotifyTimer = null;
            }
            IsWindowClosed = true;
            ShredServices.TextChat.CloseRoom(RoomName);
        }
    }

    public sealed class TextChatRoomUserInfo : ViewModelBase, IShortUserInfo, INotifyPropertyChanged
    {
        public TextChatRoomUserInfo(IShortUserInfo proto)
        {
            Contract.Requires(proto != null);

            Uid = proto.Uid;
            FirstName = proto.FirstName;
            LastName = proto.LastName;
            Country = proto.Country;
            Age = proto.Age;
            Gender = proto.Gender;
            Name = proto.Name;
            KnownLanguages = proto.KnownLanguages;
            PracticingLanguages = proto.PracticingLanguages;
        }

        public int Uid { get; private set; }
        public string FirstName { get; private set; }
        public string LastName { get; private set; }
        public Country Country { get; private set; }
        public int Age { get; private set; }
        public Gender Gender { get; private set; }
        public string Name { get; private set; }
        public IEnumerable<Language> KnownLanguages { get; private set; }
        public IEnumerable<Language> PracticingLanguages { get; private set; }

        private static readonly TimeSpan ActivityTimeout = new TimeSpan(0, 0, 1);

        public DateTime LastActivity
        {
            get { return _lastActivity; }
            set { _lastActivity = value; OnTypingChanged(); }
        }
        private DateTime _lastActivity;

        public bool IsTyping { get { return (DateTime.Now - LastActivity) < ActivityTimeout; } }

        public void OnTypingChanged()
        {
            OnPropertyChanged(() => IsTyping);
        }

        public override string ToString()
        {
            return Name ?? base.ToString();
        }
    }

    public sealed class MessageEx : IMessageEx
    {
        public MessageEx(IShortUserInfo user, bool isMyMessage, string text)
        {
            Contract.Requires(user != null);
            Contract.Requires(!String.IsNullOrEmpty(text));

            User = user;
            IsMyMessage = isMyMessage;
            Text = text;
            Time = DateTime.Now;
        }

        public IShortUserInfo User { get; private set; }
        public string FirstName { get { return User.FirstName; } }
        public bool IsMyMessage { get; private set; }
        public string Text { get; private set; }
        public DateTime Time { get; private set; }
    }
}
